package jmine.tec.script.impl.beanshell;

import static jmine.tec.script.impl.beanshell.BeanshellMessages.INTERPRETER_ERROR;
import static jmine.tec.script.impl.beanshell.BeanshellMessages.INTERPRETER_PROVIDER;

import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jmine.tec.annotations.Unmodifiable;
import jmine.tec.script.ScriptExecutionContext;
import jmine.tec.script.ScriptExecutorException;
import jmine.tec.script.impl.AbstractScriptExecutor;
import bancosys.tec.exception.AbstractException;
import bancosys.tec.exception.BusinessException;
import bsh.EvalError;
import bsh.Interpreter;
import bsh.InterpreterFactory;
import bsh.ParseException;
import bsh.TargetError;

/**
 * Script Executor para Beanshell
 * 
 * @author jrenaut
 */
@Unmodifiable
public class BeanshellScriptExecutor extends AbstractScriptExecutor {

    public static final String DEPENDENCIES_COMMAND = "setDependencies";

    public static final String VALIDATIONS_COMMAND = "setValidations";

    private final InterpreterFactory interpreterFactory = new InterpreterFactory();

    /**
     * Executa o script definido neste contexto.
     * 
     * @param <R> O tipo do Resultado
     * @param context O contexto de execução
     * @return R
     * @throws BeanshellScriptExecutorException Se não for possível executar este script
     */
    @Override
    protected <R> R doExecuteScript(final ScriptExecutionContext<R> context) throws BeanshellScriptExecutorException {

        String script = context.getScript();
        String initScript = context.getInitScript();
        try {
            Interpreter interpreter = this.getInterpreterForExecution(initScript, script);
            this.setParameterValues(context.getParameters().getParameterValues(), interpreter);
            this.setParameterValues(context.getParameters().getConstants(), interpreter);
            this.callExecuteScript(interpreter);
            return context.getResult();
        } catch (ParseException e) {
            String trace = context.getTracer();
            throw new BeanshellScriptExecutorException(INTERPRETER_ERROR.create(e.getCause(), context.getScriptId(), e,
                    this.extractParseErrorLineNumber(e), trace), e);
        } catch (TargetError e) {
            final Throwable target = e.getTarget();
            String trace = context.getTracer();
            if (target instanceof BusinessException) {
                throw (BusinessException) target;
            } else if (target instanceof AbstractException) {
                throw new BeanshellScriptExecutorException(INTERPRETER_PROVIDER.create(target, context.getScriptId(),
                        e.getErrorLineNumber(), trace), e);
            } else {
                throw new BeanshellScriptExecutorException(INTERPRETER_ERROR.create(e.getCause(), context.getScriptId(), e,
                        e.getErrorLineNumber(), trace), e);
            }
        } catch (EvalError e) {
            String trace = context.getTracer();
            throw new BeanshellScriptExecutorException(INTERPRETER_ERROR.create(e.getCause(), context.getScriptId(), e,
                    e.getErrorLineNumber(), trace), e);
        }
    }

    /**
     * Pega do cache ou cria um novo {@link Interpreter}
     * 
     * @param initScript String
     * @param script String
     * @return {@link Interpreter}
     * @throws EvalError e
     */
    protected Interpreter getInterpreterForExecution(final String initScript, final String script) throws EvalError {
        return this.interpreterFactory.getInterpreterForExecution(initScript, script);
    }

    /**
     * Sinaliza a implementação que o script deve ter suas dependências descritas.
     * 
     * @param <R> O tipo do resultado
     * @param context contexto
     * @return R
     * @throws ScriptExecutorException Se não for possível executar o script
     */
    @Override
    protected <R> R doGetDependencies(final ScriptExecutionContext<R> context) throws ScriptExecutorException {
        String script = context.getScript();
        String initScript = context.getInitScript();
        try {
            Interpreter interpreter = this.getInterpreterForDependencies(initScript, script);
            this.setParameterValues(context.getParameters().getParameterValues(), interpreter);
            this.setParameterValues(context.getParameters().getConstants(), interpreter);
            this.callDependencies(interpreter);
            return context.getResult();
        } catch (ParseException e) {
            String trace = context.getTracer();
            throw new BeanshellScriptExecutorException(
                    INTERPRETER_ERROR.create(e.getCause(), e, this.extractParseErrorLineNumber(e), trace), e);
        } catch (TargetError e) {
            final Throwable target = e.getTarget();
            String trace = context.getTracer();
            if (target instanceof BusinessException) {
                throw (BusinessException) target;
            } else if (target instanceof AbstractException) {
                throw new BeanshellScriptExecutorException(INTERPRETER_PROVIDER.create(target, e.getErrorLineNumber(), trace), e);
            } else {
                throw new BeanshellScriptExecutorException(INTERPRETER_ERROR.create(e.getCause(), e, e.getErrorLineNumber(), trace), e);
            }
        } catch (EvalError e) {
            if (e.getMessage().contains("Command not found: " + DEPENDENCIES_COMMAND + "()")) {
                return null;
            }
            String trace = context.getTracer();
            throw new BeanshellScriptExecutorException(INTERPRETER_ERROR.create(e.getCause(), e, e.getErrorLineNumber(), trace), e);
        }
    }

    /**
     * Pega do cache ou cria um novo {@link Interpreter}
     * 
     * @param initScript String
     * @param script String
     * @return {@link Interpreter}
     * @throws EvalError e
     */
    protected Interpreter getInterpreterForDependencies(final String initScript, final String script) throws EvalError {
        return this.interpreterFactory.getInterpreterForDependencies(initScript, script);
    }

    /**
     * Sinaliza a implementação que o script deve validar os dados
     * 
     * @param <R> O tipo do resultado
     * @param context contexto
     * @return R
     * @throws ScriptExecutorException Se não for possível executar o script
     */
    @Override
    protected <R> R doGetValidations(ScriptExecutionContext<R> context) throws ScriptExecutorException {
        String script = context.getScript();
        String initScript = context.getInitScript();
        try {
            Interpreter interpreter = this.getInterpreterForDependencies(initScript, script);
            this.setParameterValues(context.getParameters().getParameterValues(), interpreter);
            this.setParameterValues(context.getParameters().getConstants(), interpreter);
            this.callValidations(interpreter);
            return context.getResult();
        } catch (ParseException e) {
            String trace = context.getTracer();
            throw new BeanshellScriptExecutorException(
                    INTERPRETER_ERROR.create(e.getCause(), e, this.extractParseErrorLineNumber(e), trace), e);
        } catch (TargetError e) {
            final Throwable target = e.getTarget();
            String trace = context.getTracer();
            if (target instanceof BusinessException) {
                throw (BusinessException) target;
            } else if (target instanceof AbstractException) {
                throw new BeanshellScriptExecutorException(INTERPRETER_PROVIDER.create(target, e.getErrorLineNumber(), trace), e);
            } else {
                throw new BeanshellScriptExecutorException(INTERPRETER_ERROR.create(e.getCause(), e, e.getErrorLineNumber(), trace), e);
            }
        } catch (EvalError e) {
            if (e.getMessage().contains("Command not found: " + VALIDATIONS_COMMAND + "()")) {
                return null;
            }
            String trace = context.getTracer();
            throw new BeanshellScriptExecutorException(INTERPRETER_ERROR.create(e.getCause(), e, e.getErrorLineNumber(), trace), e);
        }
    }

    /**
     * Extrai o numero da linha em que um erro de parse ocorreu
     * 
     * @param e {@link ParseException}
     * @return int
     */
    private int extractParseErrorLineNumber(ParseException e) {
        String message = e.getMessage();
        Matcher m = PATTERN_BSH_ERROR.matcher(message);
        if (m.matches()) {
            return Integer.parseInt(m.group(1));
        }
        return -1;
    }

    /**
     * Converte os valores do mapa para primitivas se necessario e adiciona ao namespace do interpretador
     * 
     * @param params o mapa de paramtros
     * @param interpreter o interpretador
     * @throws EvalError se houver um erro?
     */
    private void setParameterValues(final Map<String, Object> params, final Interpreter interpreter) throws EvalError {
        for (final Entry<String, Object> entry : params.entrySet()) {
            PrimitiveAdapter.setAsPrimitive(interpreter, entry.getKey(), entry.getValue());
        }
    }

    /**
     * Executa o script propriamente dito
     * 
     * @param interpreter o interpreter
     * @throws EvalError se houver uma excecao ao evaluar o script
     */
    private void callExecuteScript(final Interpreter interpreter) throws EvalError {
        interpreter.eval("executeScript();");
    }

    /**
     * Executa o método <code>setDependencies</code> definido no script.
     * 
     * @param interpreter o interpreter
     * @throws EvalError se houver uma excecao ao evaluar o script
     */
    private void callDependencies(final Interpreter interpreter) throws EvalError {
        interpreter.eval(DEPENDENCIES_COMMAND + "();");
    }

    /**
     * Executa o método <code>setValidations</code> definido no script.
     * 
     * @param interpreter o interpreter
     * @throws EvalError se houver uma excecao ao evaluar o script
     */
    private void callValidations(Interpreter interpreter) throws EvalError {
        interpreter.eval(VALIDATIONS_COMMAND + "();");

    }

    public static final Pattern PATTERN_BSH_ERROR = Pattern.compile("Parse error at line (\\d+), column (\\d+).+");
}
